算法設計與分析第九周——動態規劃之Word Break II
這周我們繼續來看動態規劃,上週的題目比較簡單,目的是爲了讓自己更好地理解動態規劃和知道如何構建簡單的動態規劃狀態轉變方程,所以這周選了一道相對難一點的題目 -> 題目鏈接。
Word Break II 其實是上週做的Word Break的延伸題目,我這裏需要用到上週的算法過程。
題目詳情
題目跟Word Break類似,給出一個非空字符串s和含有多個字符串的字典集合WordDict,找出s被分爲由字典集合裏的詞語組成的所有句子,若不存則返回空集合,詞語可以重複,且字典裏的詞語不重複。
樣例說明:
輸入s | 輸入字典WordDict | 輸出 |
|
{“cat”,“cats”,“sand”,“and”,“dog”} |
{ “cats and dog”, “cat sand dog” } |
|
|
{ "pine apple pen apple", |
|
{"cats", "dog", "sand", "and", "cat"} |
{ } |
題目分析及算法設計
先來分析一下返回空集合的情況,由上週得到的 dp 數組(詳情請轉至Word Break)可知,如果s可被拆分,則 dp[s.size()] 必然爲真,而之前的函數就是判斷 s 是否可被拆分的,故只要把輸入 s 和 wordDict 傳入判斷函數,如果判斷函數返回值爲假,那麼就可以返回空集。
其他情況通過從後遍歷 s 字符串,找出所有能夠在wordDict中找到的子串,並把他們從前往後通過空格連接起來即可,構建動態規劃狀態轉換方程如下:dp[ i ] = currSubStr + dp[ j ],其中 dp 爲字符數組的數組,dp[ j ] 爲上一次從s的尾部往前找到符合子串並連接之後的字符串集合,currSubStr 爲當前找到的在 wordDictt 中存在的 s 的子串,dp[ i ] 爲當前遍歷到位置處所有滿足條件的字符串的集合。狀態轉換的條件爲 dp[ j ] 不爲空,即內部有已經找到的子串的連接而成的字符串。dp[ s.size() ] 初始化爲含有一個空字符串的集合。
算法包含兩個循環,外循環爲從s的尾部向前遍歷,內循環爲從外循環遍歷到的位置往後遍歷,以找到從 s 尾部往前的所有在wordDict裏的子串,如果 dp[ j ] 不爲空,那麼對 dp[ j ]內的所有字符串,我們都用當前獲取到的子串去連接並把他們放入 dp[ i ] 中,到最後當最外層循環遍歷完畢,dp[ 0 ] 內的結果即爲所求。
代碼詳情
bool canBreak(string s, vector<string>& wordDict) {
// dp[j] == true means s[i, j] is in the wordDict, 0 <= i < j
// dp[j] == true if dp[i] && s[i, j] is in the wordDict
bool dp[s.size() + 1];
memset(dp, false, s.size() + 1);
dp[0] = true;
set<string> dict(wordDict.begin(), wordDict.end());
for (int j = 1; j <= s.size(); j ++) {
for (int i = j - 1; i >= 0; i --) {
if (dp[i] && dict.find(s.substr(i, j - i)) != dict.end()) {
dp[j] = true;
break;
}
}
}
return dp[s.size()];
}
vector<string> wordBreak(string s, vector<string>& wordDict) {
if (!canBreak(s, wordDict)) return vector<string> ();
int len = s.size();
// dp[i] = currStr + dp[j], traverse dp from the end of s,
// dp[j] means there are some strings that can be find in the wordDict
vector<vector<string>> dp(len + 1, vector<string>());
dp[len].push_back("");
set<string> dict(wordDict.begin(), wordDict.end());
// two loops for traverse
for (int i = len - 1; i >= 0; i --) {
for (int j = i + 1; j <= len; j ++) {
string currStr = s.substr(i, j - i);
// if currStr is in the wordDict, test it
if (dict.find(currStr) != dict.end()) {
if (!dp[j].empty()) {
for (auto word : dp[j]) {
string tmp = currStr;
if (word.size()) {
// dp[i] = currStr + dp[j]
tmp = tmp + " " + word;
}
dp[i].push_back(tmp);
}
}
}
}
}
return dp[0];
}
下面使用一個具體例子來說明:輸入 s 爲 "catsanddog",wordDict爲 {“cat”,“cats”,“sand”,“and”,“dog”}
i | dp[ i ] | 說明 |
10 | { "" } |
當 i 從s尾部遍歷,dp[10] 初始化爲空字符串集合 |
7 | { "dog" } | i 遍歷到7時,因爲 j 從8往後遍歷,當就= 10,找到一個在wordDict的s的子串“dog”,又因爲 dp[10]不爲空,把“dog”加入 dp[7] 中 |
4 | { "and dog" } |
i 繼續往前遍歷,i = 4時,j 從5開始往後遍歷,當 j = 7,找到“and”,又 dp[7] 不爲空,故把“and dog”加入 dp[4] 中 |
3 | { "sand dog" } |
i = 3時,j 從4開始遍歷,當 j = 7,找到“sand”,故把“sand” + “ ” + dp[7] 的結果加入 dp[3] |
0 |
{ "cat sand dog", } |
i = 0時,j 從1開始往後遍歷,當 j = 3,檢測到“cat”,又dp[3] 不爲空,故把“cat” + “ ” + dp[3] 的結果加入 dp[0],同理當 j = 4 時也一樣 |
複雜度分析
求s能否被拆分複雜度爲 O(n^2),後面的求拆分成的字符串複雜度爲O(n^2 * d),其中 d 爲 wordDict 中詞語的個數。故總的複雜度爲O(n^2 * d)。
謝謝閱讀。